[toc]
暴力破解分析
分析思路
- 找到注册的窗口
- 测试注册窗口的反应
- 根据反应做出下一步分析的打算
- 猜测 API,API 下断动态调试
- 挑出敏感字符串,在程序中搜索
- 动态分析,定位关键跳,修改代码
- 动态分析,定位关键
call
,修改代码
尝试注册
安装 010 Editor v8.0.1 版本,完成后会提示评估版本试用期是30天,需要我们注册
找到注册窗口并试着注册,发现会有错误提示,名称或密码无效
分析注册窗口
第一种方法是查找弹出窗口的 API
先在 OD 中附加程序,找到主模块
查找当前模块中的名称
Ctrl+N
,发现有很多导入函数,使用的是 Qt5 的动态库,由于不清楚 Qt 库函数,故无法定位用的是什么 Qt 窗口函数实际上不论什么界面库,能弹出窗口最终调用的都是 API 函数,可以查找和窗口有关的动态库模块
user32.dll
再查找当前模块中的名称,其中和创建窗口有关的 API 有
CreateWindow
,MessageBox
,DialogBox
,CreateDialog
等若不能确定调用的是哪个 API 就在所有的 API 下断,Qt 的窗口函数底层应该调用
CreateWindow
,尝试后发现是CreateWindowExW
这个 API,下断,运行点击 K,栈回溯分析
发现有一个
show
函数,调用来自主模块,我们主要分析的也是主模块,依次双击进入,查看附近代码有无可疑或敏感的字符串,若没有就点击下一个,直到找到提示无效的密码发现这儿有一个跳转,查看下面的信息框,根据跳转指示可以知道是哪儿跳转来的,然后一步一步往上跟,进行动态分析
第二种方法是搜索字符串
首先注册,弹出名称或密码无效的提示,
Ctrl+C
赋值字符串到记事本,在 OD 中右击中文搜索引擎,搜索 ASCII,再右击查找,输入部分字符串,确定后就能找到,再查找下一个,看看有没有多个相同的字符串
动态分析
找到正确的字符串,再往上跟,分析各个跳转指令的条件
找到关键跳,不能跳过正确的字符串
再往上,找到该跳转来源,即关键函数,将返回值改为 eax = 0x2D
就爆破成功
但先要去掉随机地址,用 010 Editor 打开,修改随机地址 4081 为 0081,再重新加载 OD
进入关键函数,修改
然后保存,打开新保存的程序,授权成功
算法分析
分析思路
- 单步跟踪,找到访问用户名密码的代码
- 一步一步分析,加注释
- 反复推敲,找规律
- 写代码验证
算法验证
进入关键函数,遇到第一个功能 CALL,经调试可发现功能是密码字符串转16进制字节数据,它之上的两个函数分别是判断输入的用户名和密码是否为空,若有一个为空就退出
继续往下跟踪分析,可以看到将密码的16进制数据进行一系列运算判断
K[3] 的值可以有3个,0x9C / 0xFC /0xAC,分别对应不同的算法
以下是值为 0x9C 的算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37013BDC5D . 8A5D DF MOV BL,BYTE PTR SS:[EBP-0x21] ; BL = K[3]
013BDC60 . 8A7D E1 MOV BH,BYTE PTR SS:[EBP-0x1F] ; BH = K[5]
013BDC63 . 80FB 9C CMP BL,0x9C ; Switch (cases 9C..FC)
013BDC66 . 75 70 JNZ SHORT 010Edito.013BDCD8 ; K[3] 应为 0x9C 0xFC 0xAC
013BDC68 . 8A45 DC MOV AL,BYTE PTR SS:[EBP-0x24] ; AL = K[0]; Case 9C of switch 013BDC63
013BDC6B . 3245 E2 XOR AL,BYTE PTR SS:[EBP-0x1E] ; AL = K[0] ^ K[6]
013BDC6E . 8845 E8 MOV BYTE PTR SS:[EBP-0x18],AL ; [ebp - 0x18] = AL
013BDC71 . 8A45 DD MOV AL,BYTE PTR SS:[EBP-0x23] ; AL = K[1]
013BDC74 . 3245 E3 XOR AL,BYTE PTR SS:[EBP-0x1D] ; AL = K[1] ^ K[7]
013BDC77 . FF75 E8 PUSH DWORD PTR SS:[EBP-0x18] ; K[0] ^ K[6]
013BDC7A . 0FB6C8 MOVZX ECX,AL ; ecx = K[1] ^ K[7] & 0xFF 0扩展填充
013BDC7D . B8 000100>MOV EAX,0x100 ; eax = 0x100
013BDC82 . 66:0FAFC8 IMUL CX,AX ; cx = ( K[1] ^ K[7] & 0xFF ) * 0x100
013BDC86 . 8A45 DE MOV AL,BYTE PTR SS:[EBP-0x22] ; AL = K[2]
013BDC89 . 32C7 XOR AL,BH ; AL = K[2] ^ K[5]
013BDC8B . 0FB6C0 MOVZX EAX,AL ; eax = K[2] ^ K[5] & 0xFF
013BDC8E . 66:03C8 ADD CX,AX ; cx = ( K[1] ^ K[7] & 0xFF ) * 0x100 + K[2] ^ K[5] & 0xFF
013BDC91 . 0FB7F1 MOVZX ESI,CX ; esi=( (K[1]^K[7]&0xFF)*0x100+K[2]^K[5]&0xFF )&0xFFFF
013BDC94 . E8 AB9904>CALL 010Edito.00407644 ; AL = ( K[0]^K[6]^0x18 + 0x3D ) ^ 0xA7
013BDC99 . 0FB6C0 MOVZX EAX,AL
013BDC9C . 56 PUSH ESI ; 010Edito.02E64618
013BDC9D . 8947 1C MOV DWORD PTR DS:[EDI+0x1C],EAX ; [edi+0x1c] = eax
013BDCA0 . E8 23A704>CALL 010Edito.004083C8 ; 计算后判断余数是否为0,若是则返回商,否则返回0
013BDCA5 . 8B4F 1C MOV ECX,DWORD PTR DS:[EDI+0x1C] ; ecx = [edi+0x1c] = ( K[0]^K[6]^0x18 + 0x3D ) ^ 0xA7
013BDCA8 . 83C4 08 ADD ESP,0x8
013BDCAB . 0FB7C0 MOVZX EAX,AX ; eax = (((esi^0x7892)+0x4D30)^0x3421)&0xFFFF / 0xB 或 0
013BDCAE . 8947 20 MOV DWORD PTR DS:[EDI+0x20],EAX
013BDCB1 . 85C9 TEST ECX,ECX
013BDCB3 . 0F84 BC01>JE 010Edito.013BDE75 ; 若 ecx=0,则 eax=0xE7 退出
013BDCB9 . 85C0 TEST EAX,EAX
013BDCBB . 0F84 B401>JE 010Edito.013BDE75 ; 若 eax=0,则 eax=0xE7 退出,故上个函数的返回值不能为0
013BDCC1 . 3D E80300>CMP EAX,0x3E8
013BDCC6 . 0F87 A901>JA 010Edito.013BDE75 ; eax <= 0x3E8 否则 eax=0xE7 退出
013BDCCC . 83F9 02 CMP ECX,0x2 ; 判断 ecx
013BDCCF . 1BF6 SBB ESI,ESI ; 010Edito.02E64618
013BDCD1 . 23F1 AND ESI,ECX ; esi = 0
013BDCD3 . E9 B30000>JMP 010Edito.013BDD8B以下是值为 0xFC 的算法:
1
2
3
4
5
6
7013BDCD8 > \80FB FC CMP BL,0xFC
013BDCDB . 75 1F JNZ SHORT 010Edito.013BDCFC
013BDCDD . C747 1C FF000>MOV DWORD PTR DS:[EDI+0x1C],0xFF ; Case FC of switch 013BDC63
013BDCE4 . BE FF000000 MOV ESI,0xFF ; esi = 0xFF
013BDCE9 . C747 20 01000>MOV DWORD PTR DS:[EDI+0x20],0x1 ; [edi+0x20] = 1
013BDCF0 . C747 30 01000>MOV DWORD PTR DS:[EDI+0x30],0x1 ; [edi+0x30] = 1
013BDCF7 . E9 8F000000 JMP 010Edito.013BDD8B以下是值为 0xAC 的算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42013BDCFC > \80FB AC CMP BL,0xAC
013BDCFF . 0F85 70010000 JNZ 010Edito.013BDE75
013BDD05 . 8A45 DD MOV AL,BYTE PTR SS:[EBP-0x23] ; AL = K[1]; Case AC of switch 013BDC63
013BDD08 . 3245 E3 XOR AL,BYTE PTR SS:[EBP-0x1D] ; AL = K[1] ^ K[7]
013BDD0B . 0FB6C8 MOVZX ECX,AL ; ecx = AL & 0xFFFF
013BDD0E . B8 00010000 MOV EAX,0x100 ; eax = 0x100
013BDD13 . 66:0FAFC8 IMUL CX,AX ; cx = cx * ax
013BDD17 . 8A45 DE MOV AL,BYTE PTR SS:[EBP-0x22] ; AL = K[2]
013BDD1A . 32C7 XOR AL,BH ; AL = AL ^ BH
013BDD1C . C747 1C 02000>MOV DWORD PTR DS:[EDI+0x1C],0x2 ; [edi+0x1C] = 2
013BDD23 . 0FB6C0 MOVZX EAX,AL ; eax = AL & 0xFFFF
013BDD26 . 66:03C8 ADD CX,AX ; cx = cx + ax
013BDD29 . 0FB7C1 MOVZX EAX,CX ; eax = cx & 0xFFFF
013BDD2C . 50 PUSH EAX
013BDD2D . E8 96A604FF CALL 010Edito.004083C8 ; 同 K[3]=0x9C 中的判断余数
013BDD32 . 0FB7C0 MOVZX EAX,AX ; eax = (((eax^0x7892)+0x4D30)^0x3421)&0xFFFF / 0xB 或 0
013BDD35 . 83C4 04 ADD ESP,0x4
013BDD38 . 8947 20 MOV DWORD PTR DS:[EDI+0x20],EAX ; [edi+0x20] = eax
013BDD3B . 85C0 TEST EAX,EAX ; eax != 0 故上个函数计算后的余数应为0
013BDD3D . 0F84 32010000 JE 010Edito.013BDE75
013BDD43 . 3D E8030000 CMP EAX,0x3E8 ; 且 eax <= 0x3E8
013BDD48 . 0F87 27010000 JA 010Edito.013BDE75
013BDD4E . 0FB655 E5 MOVZX EDX,BYTE PTR SS:[EBP-0x1B] ; edx = K[9] & 0xFFFF
013BDD52 . 0FB64D E0 MOVZX ECX,BYTE PTR SS:[EBP-0x20] ; ecx = K[4] & 0xFFFF
013BDD56 . 0FB6C7 MOVZX EAX,BH ; eax = K[5] & 0xFFFF
013BDD59 . 33D0 XOR EDX,EAX ; edx = edx ^ eax
013BDD5B . 0FB645 E4 MOVZX EAX,BYTE PTR SS:[EBP-0x1C] ; eax = K[8] & 0xFFFF
013BDD5F . 33C8 XOR ECX,EAX ; ecx = ecx ^ eax
013BDD61 . C1E2 08 SHL EDX,0x8 ; edx << 8
013BDD64 . 0FB645 E2 MOVZX EAX,BYTE PTR SS:[EBP-0x1E] ; eax = K[6] & 0xFFFF
013BDD68 . 03D1 ADD EDX,ECX ; edx += ecx
013BDD6A . 0FB64D DC MOVZX ECX,BYTE PTR SS:[EBP-0x24] ; ecx = K[0] & 0xFFFF
013BDD6E . C1E2 08 SHL EDX,0x8 ; edx << 8
013BDD71 . 33C8 XOR ECX,EAX ; ecx = ecx ^ eax
013BDD73 . 03D1 ADD EDX,ECX ; edx += ecx
013BDD75 . 68 278C5B00 PUSH 010Edito.005B8C27 ; CCCCCCCC......
013BDD7A . 52 PUSH EDX
013BDD7B . E8 0BCA04FF CALL 010Edito.0040A78B ; 同 K[3]=FC 的那个函数
013BDD80 . 83C4 08 ADD ESP,0x8
013BDD83 . 8945 F0 MOV DWORD PTR SS:[EBP-0x10],EAX
013BDD86 . 8947 34 MOV DWORD PTR DS:[EDI+0x34],EAX
013BDD89 . 8BF0 MOV ESI,EAX ; [ebp-0x10] = [edi+0x34] = esi = eax
若以上算法成立,都会跳转到 0x13BDD8B,继续进行运算
对于 K[3] = 0x9C 来说
先是将用户名转换为 ASCII 码,然后进行加密计算,加密函数有4个参数,第四个参数
DS:[EDI+0x20]
来自之前的运算结果第三第二个参数默认为0和1,第一个参数是用户名字符串
加密之后分别比较 K[4] / K[5] / K[6] / K[7] 与加密后再计算的值,最后比较
[EDI+0x1C] >= 9
,若成立就正确其中
[EDI+0x1C]
也来自之前运算的结果
若 [edi+0x1C] >= 9
成立,就跳转,之后往下走成功
对于 K[3] = 0xFC 来说
传入的4个参数与上面不同,第四个参数 DS:[EDI+0x20]
恒为1
第三个参数恒为 0xFF,第二个参数恒为0,第一个参数是用户名字符串
加密之后分别比较 K[4] / K[5] / K[6] / K[7] 与加密后再计算的值,与 0x9C 不同的是,比较完 K[7] 后,由于 BL = 0xFC,故需要跳转到下面进行运算
这个函数传入2个参数,一个是之前加密函数的运算结果,另一个是 K[2]K[1]K[0] 组成的字符,进入函数内部
1 | eax = 0xF0F0F0F1 |
若想实现 eax = edx
,需 ecx == eax
,即
((ecx ^ edx ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF
==
(0xF0F0F0F1 * (((ecx ^ edx ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF) & 0xFFFF0000 >> 32) >> 4
+
(0xF0F0F0F1 * (((ecx ^ edx ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF) & 0xFFFF0000 >> 32)
其中,参数1是 ecx,参数2是 edx
那么 0x13BDE1F 不会跳转,接着往下走就会成功
对于 K[3] = 0xAC 来说
首先计算
eax = (((K[1] ^ k[7] & 0xFF) * 0x100) & 0xFF + (k[2] ^ k[5] & 0xFF)) & 0xFFFF
eax = (((eax ^ 0x7892) + 0x4D30) ^ 0x3421) & 0xFFFF
要求 eax % 0xB == 0 && eax / 0xB <= 0x3E8
成立
然后计算
edx = (((K[9] ^ K[5] << 8) + (K[4] ^ K[8])) << 8) + (K[0] ^ K[6])
其中 K[x] = K[x] & 0xFFFF
进入函数 0x0040A78B,第一个参数为 edx,第二个参数为 0x5B8C27
((ecx ^ 0x5B8C27 ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF
==
(0xF0F0F0F1 * (((ecx ^ 0x5B8C27 ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF) & 0xFFFF0000 >> 32) >> 4
+
(0xF0F0F0F1 * (((ecx ^ 0x5B8C27 ^ 0x22C078) - 0x2C175) ^ 0xFFE53167 & 0xFFFFFF) & 0xFFFF0000 >> 32)
其中 ecx = 参数1
先是将用户名转换为 ASCII 码,然后进行加密计算,加密函数有4个参数,第四个参数 DS:[EDI+0x20]
来自之前的运算结果
第三第二个参数默认为0和1,第一个参数是用户名字符串
加密之后分别比较 K[4] / K[5] / K[6] / K[7] 与加密后再计算的值,与 0x9C 不同的是,比较完 K[7] 后,由于 BL = 0xAC,故需要跳过比较 0x9C 和 0xFC
跳到这儿
[ebp-0x10] 来自此函数,故需要函数的计算结果不为0,与 K[3] = 0xFC 中那个相同函数 0x0040A78B 一样
0x4389 来自关键函数的第二个参数
注册机编写
以 K[3] = 0x9C 为例
关键一点是对用户名字符串的加密函数,想要简单地编写函数,可以使用 IDA Pro,找到该函数入口地址 0x13BD120,按 F5 得到 C 代码,稍微修改一下就可用了
函数中用到的数组可以记下它的地址 0x2E64148
转到 OD 中进行拷贝,当然也可选择 Word 类型
1 | // decode010.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 |
算法总结
针对 K[3] = 0x9C :
判断用户名密码是否为空
将密码字符串转为16进制字节数据
验证密码16进制数据
AL = ( K[0] ^ K[6] ^ 0x18 + 0x3D ) ^ 0xA7 != 0
esi = ( ( K[1] ^ K[7] & 0xFF ) * 0x100 + K[2] ^ K[5] & 0xFF ) & 0xFFFF
eax = ((( esi ^ 0x7892 ) + 0x4D30 ) ^ 0x3421 ) & 0xFFFF / 0xB
且 ( eax % 0xB == 0 && eax / 0xB <= 0x3E8 )
将用户名转为 ASCII 码
使用用户名计算出一个 key,进行一系列操作后要和密码 K[4] ~ K[7] 相等
K[4] = key & 0xFF;
K[5] = key >> 8 & 0xFF;
K[6] = key >> 16 & 0xFF;
K[7] = key >> 24 & 0xFF;判断参数 ( K[0]^K[6]^0x18 + 0x3D ) ^ 0xA7 >= 9
注册机总结
- 版本一
- 随机字节,穷举找出符合条件的值
- 版本二
- 指定用户名,调用加密函数求出 key
- 将 key 拆分,指定到密码字节数组
网络验证分析
输入正确的用户名密码后会有网络验证
在关键函数后的网络验证函数中修改跳转 je 为 jmp
在下一个函数中修改编辑为就能过掉网络验证